iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0

大家好,歡迎來到 FastAPI 系列文章的第 6 天!今天我們要探討另一種平行處理的方式:Thread(執行緒)Process(程序)

有些地方會把 Thread 翻譯成「線程」,把 Process 翻譯成「進程」。

三種並發模式的選擇

在 Python 的世界裡,我們有三種主要的並發處理方式:

  1. Asyncio(協程):適合大量 I/O 操作的高併發處理
  2. Thread(執行緒):適合 I/O 密集型任務,特別是與傳統同步程式碼整合
  3. Process(程序):適合 CPU 密集型任務,需要真正的平行計算

每種方式都有其獨特的適用場景,理解它們的差異和適用時機是構建高效能 FastAPI 應用的關鍵。

Thread(執行緒):I/O 密集型任務的好夥伴

執行緒 是作業系統中最小的執行單位,多個執行緒可以在同一個程序中並行執行。在 Python 中,執行緒特別適合處理 I/O 密集型任務,因為當一個執行緒在等待 I/O 操作時,其他執行緒可以繼續工作。

執行緒的適用場景

執行緒最適合以下情況:

  • 網路請求:呼叫外部 API 或下載檔案
  • 檔案操作:讀寫大量檔案
  • 資料庫查詢:特別是使用傳統同步資料庫驅動時
  • 與現有同步程式碼整合:當你有大量現有的同步程式碼需要並行執行

實際應用範例

在 Python 中,我們使用 threading 模組來操作執行緒:

import threading
import time
import os

def worker(worker_id):
    # os.getpid() 取得行程ID,threading.get_ident() 取得執行緒ID
    print(f"Worker {worker_id} - Process ID: {os.getpid()}, Thread ID: {threading.get_ident()} - 開始工作")
    time.sleep(2)
    print(f"Worker {worker_id} - Process ID: {os.getpid()}, Thread ID: {threading.get_ident()} - 完成工作")

if __name__ == "__main__":
    print("主執行緒開始")
    
    # 建立兩個執行緒
    t1 = threading.Thread(target=worker, args=(1,))
    t2 = threading.Thread(target=worker, args=(2,))
    
    # 啟動執行緒
    t1.start()
    t2.start()
    
    print("主執行緒繼續執行...")
    
    # 等待所有子執行緒結束
    t1.join()
    t2.join()
    
    print("主執行緒結束")

你會發現,兩個執行緒的 Process ID 都跟主程式是一樣的,但 Thread ID 會不同。

執行緒的優勢在於它能夠有效利用 I/O 等待時間。當一個執行緒在等待網路回應時,CPU 可以切換到其他執行緒繼續工作,從而提高整體效能。

Process(程序):CPU 密集型任務的解決方案

程序 是作業系統中資源分配的基本單位,每個程序都有獨立的記憶體空間。程序間的通訊比執行緒複雜,但它們可以真正並行執行,不受 Python GIL 的限制。

程序的適用場景

程序最適合以下情況:

  • 數學計算:複雜的數值運算或演算法
  • 資料處理:大量資料的轉換和分析
  • 圖像/影片處理:需要大量 CPU 運算的媒體處理
  • 機器學習:模型訓練和推論計算

CPU 密集型任務範例

import multiprocessing
import time
import os

def worker(worker_id):
    # os.getpid() 可以取得當前行程的 ID
    print(f"Worker {worker_id} - Process ID: {os.getpid()} - 開始工作")
    time.sleep(2)
    print(f"Worker {worker_id} - Process ID: {os.getpid()} - 完成工作")

if __name__ == "__main__":
    print(f"主行程 - Process ID: {os.getpid()} - 開始")
    
    # 建立兩個程序
    p1 = multiprocessing.Process(target=worker, args=(1,))
    p2 = multiprocessing.Process(target=worker, args=(2,))
    
    # 啟動程序
    p1.start()
    p2.start()
    
    print("主行程繼續執行...")
    
    # 等待所有子程序結束
    p1.join()
    p2.join()
    
    print("主行程結束")

程序的優勢在於它們可以充分利用多核心 CPU,實現真正的平行計算,不受 Python GIL 的限制。你會看到兩個子程序都有不同的 Process ID,它們是完全獨立運行的。

我該用哪個?

這是一個經典問題,答案取決於你的「任務類型」:

  • I/O 密集型 (I/O-bound):任務大部分時間都在等待外部資源,例如:網路請求、讀寫資料庫、操作檔案。CPU 其實很閒。
    • 推薦使用 ThreadAsyncio。當一個執行緒在等待 I/O 時,Python 會把它切換掉,讓 CPU 去執行其他執行緒,從而提升效率。
  • CPU 密集型 (CPU-bound):任務需要大量的 CPU 運算,例如:複雜的數學計算、影像辨識、模型訓練。
    • 務必使用 Process。因為 GIL 的存在,多執行緒對 CPU 密集任務完全沒幫助。必須使用多行程,才能將任務分配到不同 CPU 核心上,實現真正的平行加速。

小結

Thread 和 Process 為 FastAPI 提供了處理不同類型工作負載的強大能力。執行緒適合 I/O 密集型任務,程序適合 CPU 密集型任務,而 Asyncio 則適合高併發的 I/O 處理。

理解這三種並發模式的特性和適用場景,能夠幫助我們構建既高效又穩定的 FastAPI 應用。在實際開發中,最好的策略往往是根據具體需求靈活組合使用這些技術。

明天我們將探討 GIL(Global Interpreter Lock),深入了解為什麼在某些情況下執行緒的效能會受到限制,以及如何正確應對這個挑戰!


上一篇
[Day 05] Asyncio
系列文
用 FastAPI 打造你的 AI 服務6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言